今天將深入探討如何使用 ThemeExtension
來實現 Flutter 應用程式的多主題管理。多主題設計的價值遠不止於提供基本的深色與淺色模式。它更是一種強大的工具,能讓你的應用程式跳脫千篇一律的框架,注入獨特的品牌個性,並讓使用者能夠自由選擇最貼近個人風格的視覺體驗。
除此之外,多主題還能帶來更多實際價值:
之前,要實現多主題切換,我們可能需要手動定義多個 ThemeData
物件,並在其中重複撰寫相同的屬性。然而,當你需要自定義顏色、字體、甚至是間距等不在 ThemeData
預設屬性中的內容時,程式碼便會變得複雜且難以維護。
ThemeExtension
提供了一個優雅的解決方案:它允許你在 ThemeData
上添加任何你想要的自訂屬性,並將其與 Widget Tree
緊密整合。這不僅確保了程式碼的清晰度,更提供了以下優點:
ThemeExtension
是 Flutter 框架的一部分,與 Theme.of(context)
完美整合,不需要額外的套件。ThemeData
,ThemeExtension
可以針對特定屬性進行細緻過渡,讓顏色或字體切換更自然。Extension
,降低耦合度,讓團隊協作與後續維護更輕鬆。簡單來說,它就像是為你的主題添加一個可擴充的、自訂的「工具箱」,你想要放什麼進去,都可以,並且能隨著專案成長保持結構清晰。
特性 | 只使用 ThemeData | 使用 ThemeExtension |
---|---|---|
自訂屬性 | 不支援。只能使用預設屬性。 | 完全支援。可以擴充任何自訂屬性。 |
類型安全 | 非類型安全。容易誤用預設屬性。 | 類型安全。編譯時可檢查錯誤。 |
可擴展性 | 差。隨著應用複雜度增加,程式碼會變混亂。 | 優異。可以創建多個 Extension,結構清晰。 |
取用方式 | Theme.of(context).colorScheme.primary |
Theme.of(context).extension<AppThemeColors>()!.brandColor |
主題切換 | 顏色切換較生硬,難以自訂過渡動畫。 | 支援平滑的過渡動畫,效果更佳。 |
適用情境 | 簡單、小型、不含太多客製化風格的應用程式。 | 所有需要自訂品牌風格、多主題、或複雜設計系統的應用程式。 |
設計師/開發協作 | 只適合開發者自行調整 UI。 | 可對應設計稿中的「品牌色、字體階層、陰影、邊框樣式」等,讓設計系統更好落地。 |
好的 🙆♀️
我已經幫你把 建立資料夾結構 的章節插到文章 「程式碼實作」之前,完整調整後的段落如下:
在專案中,保持清晰的資料夾結構能大幅提升可維護性與可擴充性。以下是針對多主題管理所建議的結構:
lib/
│
├─ themes/ # 與主題相關的檔案集中管理
│ ├─ app_theme_extension.dart # 自訂 ThemeExtension,定義顏色、字體等
│ ├─ app_themes.dart # 多種主題定義(藍色、紫色、綠色等)
│
└─ main.dart # 應用程式入口,載入主題並組合整體架構
這個範例會建立一個可切換多種主題的頁面,並且 UI 顏色會根據當前主題自動更新。由於前面定義的主題樣式太過複雜,所以這邊有另外訂幾個主題進行示範~
app_theme_extension.dart
)首先,我們需要建立一個新的 Dart 檔案,來定義客製化的主題屬性。這裡我們以顏色為例,因此類別命名為 AppThemeColors
,並繼承自 ThemeExtension<AppThemeColors>
。
// app_theme_extension.dart
import 'package:flutter/material.dart';
@immutable
class AppThemeColors extends ThemeExtension<AppThemeColors> {
const AppThemeColors({
required this.brandColor,
required this.textColor,
});
final Color brandColor;
final Color textColor;
// 覆寫 copyWith 與 lerp,支援屬性更新與動畫過渡
@override
AppThemeColors copyWith({
Color? brandColor,
Color? textColor,
}) {
return AppThemeColors(
brandColor: brandColor ?? this.brandColor,
textColor: textColor ?? this.textColor,
);
}
@override
AppThemeColors lerp(ThemeExtension<AppThemeColors>? other, double t) {
if (other is! AppThemeColors) return this;
return AppThemeColors(
brandColor: Color.lerp(brandColor, other.brandColor, t)!,
textColor: Color.lerp(textColor, other.textColor, t)!,
);
}
}
在這個類別中,我們定義了兩個自訂顏色:brandColor 和 textColor。
關鍵在於 覆寫 copyWith
與 lerp
:
copyWith
讓我們能靈活更新單一屬性。lerp
讓 Flutter 在主題切換時能正確處理顏色漸變,避免生硬的閃跳。app_themes.dart
)接下來,我們定義三種不同的主題:藍色、紫色以及一個綠色主題。在這些主題中,我們將使用 ThemeData
的 extensions
屬性來加入我們剛剛創建的 AppThemeColors
。
// app_themes.dart
import 'package:flutter/material.dart';
import 'app_theme_extension.dart';
final ThemeData blueTheme = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
extensions: const <ThemeExtension<dynamic>>[
AppThemeColors(
brandColor: Colors.blue,
textColor: Colors.black,
),
],
);
final ThemeData purpleTheme = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple),
extensions: const <ThemeExtension<dynamic>>[
AppThemeColors(
brandColor: Colors.purple,
textColor: Colors.white,
),
],
);
final ThemeData greenTheme = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
extensions: const <ThemeExtension<dynamic>>[
AppThemeColors(
brandColor: Colors.green,
textColor: Colors.green.shade900,
),
],
);
main.dart
)最後,我們在主應用程式中實現多主題切換的邏輯。後續可以搭配 Riverpod 進行狀態管理,能有效將主題邏輯與 UI 分離,讓多主題切換的程式碼更清晰、更易於維護且效能更好。
// main.dart
import 'package:flutter/material.dart';
import 'app_theme_extension.dart';
import 'app_themes.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final List<ThemeData> _themes = [blueTheme, darkTheme, greenTheme];
final List<String> _themeNames = ['藍色主題', 'purpleTheme', '綠色主題'];
final List<IconData> _themeIcons = [Icons.light_mode, Icons.dark_mode, Icons.color_lens];
int _themeIndex = 0;
void _toggleTheme() {
setState(() {
_themeIndex = (_themeIndex + 1) % _themes.length;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '多主題切換範例',
theme: _themes[_themeIndex],
home: ThemeSwitchPage(
toggleTheme: _toggleTheme,
currentThemeName: _themeNames[_themeIndex],
currentThemeIcon: _themeIcons[_themeIndex],
),
);
}
}
class ThemeSwitchPage extends StatelessWidget {
const ThemeSwitchPage({
super.key,
required this.toggleTheme,
required this.currentThemeName,
required this.currentThemeIcon,
});
final VoidCallback toggleTheme;
final String currentThemeName;
final IconData currentThemeIcon;
@override
Widget build(BuildContext context) {
// 取得自訂的 ThemeExtension
final AppThemeColors themeColors =
Theme.of(context).extension<AppThemeColors>()!;
return Scaffold(
appBar: AppBar(
title: const Text('多主題切換範例'),
backgroundColor: themeColors.brandColor, // 使用自訂的顏色
actions: [
IconButton(
icon: Icon(currentThemeIcon),
onPressed: toggleTheme,
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'目前主題:$currentThemeName',
style: TextStyle(
color: themeColors.textColor, // 使用自訂的顏色
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
Text(
'點擊右上角的按鈕來切換主題',
style: TextStyle(
color: themeColors.textColor, // 使用自訂的顏色
fontSize: 20,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(24),
color: Theme.of(context).colorScheme.surface,
child: Text(
'這個卡片的背景色會隨著主題改變',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
),
],
),
),
);
}
}
demo | trip |
---|---|
![]() |
![]() |